Generate specialized encoder functions at compile time (3-6x faster encoding) - #449
Open
csWen wants to merge 1 commit into
Open
Generate specialized encoder functions at compile time (3-6x faster encoding)#449csWen wants to merge 1 commit into
csWen wants to merge 1 commit into
Conversation
Protobuf.encode/1 interpreted MessageProps on every call: per-field
dispatch on metadata, presence checks, and IO.iodata_length/1 walks to
write length prefixes. For large messages this interpretation overhead
dominates encoding time.
The DSL now compiles each message's field metadata into a specialized
__encode_sized__/1, resolving dispatch, presence checks, field tags,
and wire types at compile time. Encoders return {iodata, byte_size} so
parents write length prefixes without re-walking the iodata, and map
entries are encoded straight from key/value pairs via
__encode_entry_sized__/2 without building entry structs.
The interpreter remains for messages with a transform module and for
values the generated clauses reject, and failed messages are replayed
through it so Protobuf.EncodeError still names the failing field.
Varint.encode/1 now builds each varint as a single binary in one
bit-syntax instruction and rejects integers that don't fit in 64 bits
instead of silently truncating them.
Encoding is 3-6x faster depending on message shape, with about half the
allocations. Wire output is byte-identical; the conformance suite
passes unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the proposal from #448.
What changes
Protobuf.encode/1currently interpretsMessagePropson every call: per-field dispatch on metadata, presence checks, andIO.iodata_length/1walks to write length prefixes. This PR moves that work to compile time: the DSL compiles each message into a specialized__encode_sized__/1that returns{iodata, byte_size}, so dispatch, presence checks, tags, and wire types are resolved once at compile time and parents write length prefixes without re-walking the iodata. Map entries are encoded straight from key/value pairs, without building entry structs.The interpreter stays for what isn't worth specializing: messages with a
transform_module, values the generated clauses reject, and error reporting — a failing message is replayed through the interpreter soProtobuf.EncodeErrornames the failing field exactly as before.Varint.encode/1now builds each varint as a single binary in one bit-syntax instruction, and rejects integers that don't fit in 64 bits instead of silently truncating them.Results
Google benchmark datasets (Apple M1 Max, Elixir 1.18.2 / OTP 26):
The production ~8 MB message from #448 goes from 716 ms to 223 ms (3.2x) — large messages sit at the lower end of the range since raw byte copying takes a bigger share of the time.
Costs
Every message module now carries its generated encoder: compiling this repo's test suite goes from ~3.8 s to ~8.9 s, and ebin grows from ~2.8 MB to ~3.8 MB. Decoding is untouched.
Compatibility
__encode_sized__/1is a generated internal, everything still goes throughProtobuf.encode/1.